目前我們已經做出台股加權指數的 K 線圖,但目前進度的線圖的 x 軸沒有時間,所以當使用者看到這張圖,無法判斷這張圖每根 K 線的日期,是哪一天。所以我們需要轉換 x 軸的 index 到 人類可閱讀的日期。
開始之前
因為一個月的交易量大約是 20 日左右,所以當月加上個月的量,也在 40 根左右。所以再加上 2 個月前的 K 線資料,讓資料的數量比較充足。
先擴充 DateUtility ,補上一個 func,輸入 n,回傳與現在日期差 n 個月的第一天 Date。
func getMonthStartDate(date: Date = Date(), add month: Int) -> Date {
let calendar = isoCalendar
let startOfMonth = getStartOfMonth(date: date)
return calendar.date(byAdding: DateComponents(month: month), to: startOfMonth) ?? Date()
}
所以要拿兩個月前的資料,就只是下面這一行
let date = dateUtility.getMonthStartDate(date: Date(), add: -2)
然後在 TwStockMarketKLineModel 發動拿取前三個月
/// 會取這個月和前一個月台股加權指的 KLine data,單一個月,有可能 k 棒數量太少
func requestTwExKLineInfo() {
requestTwExThisMonthKLineInfo() //實作在前面的文章已有
requestTwExLastMonthKLineInfo() //實作在前面的文章已有
requestTwExBefore2MonthKLineInfo()
}
/// 拿前兩個月的 k line
private func requestTwExBefore2MonthKLineInfo() {
let date = dateUtility.getMonthStartDate(date: Date(), add: -2)
manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
self?.update(kLineDataSet)
self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
}
}
當畫線的實作已經確認完成了之後,就是整理程式碼。
先開一個 ChartsAdapter,讓這個物件負責整個專案和 Charts 溝通。那首先,把 KLine VC 中和 Charts 相關功能,放進去。
import UIKit
import Charts
class ChartsAdapter {
}
// MARK: - 這一段的程式碼做 K Line charts
extension ChartsAdapter {
/// 讓 VC 在需要 K Line 圖的時候直接拿到一個 K Line View,但為了不讓外部看到 Charts,回傳 UIView
/// - Returns: 因 CandleStickChartView 繼承 UIView,封裝起來,不讓外部看到 Charts
func getCandleStickChartView() -> UIView {
let candleView = CandleStickChartView()
setupCandleStickView(candleView)
return candleView
}
func update(stockSticks: [StockKLine], on candleView: UIView) {
let dateUtility = DateUtility()
if let candleView = candleView as? CandleStickChartView {
let dataEntry = convert(stockStick: stockSticks)
let dataSet = convert(dataEntry: dataEntry)
let data = convert(dataSet: dataSet)
candleView.data = data
updateMaxMin(candleView, dataSet: dataSet)
}
}
private func updateXAxis(_ chartView: CandleStickChartView, indexDateLabels: [Int: String]) {
chartView.xAxis.valueFormatter = CandleXAxisValueFormatter(indexLabelMap: indexDateLabels)
chartView.xAxis.granularity = 1.0
}
private func setupCandleStickView(_ chartView: CandleStickChartView) {
chartView.dragEnabled = false
chartView.setScaleEnabled(true)
chartView.maxVisibleCount = 200
chartView.pinchZoomEnabled = true
chartView.legend.horizontalAlignment = .right
chartView.legend.verticalAlignment = .top
chartView.legend.orientation = .vertical
chartView.legend.drawInside = false
chartView.legend.font = UIFont.systemFont(ofSize: 10)
chartView.leftAxis.labelFont = UIFont.systemFont(ofSize: 10)
chartView.leftAxis.spaceTop = 0.3
chartView.leftAxis.spaceBottom = 0.3
chartView.leftAxis.axisMinimum = 0
chartView.rightAxis.enabled = false
chartView.xAxis.labelPosition = .bottom
chartView.xAxis.labelFont = UIFont.systemFont(ofSize: 10)
chartView.xAxis.labelCount = 10
}
private func convert(stockStick: [StockKLine]) -> [CandleChartDataEntry] {
var dataEntry = [CandleChartDataEntry]()
for (i, each) in stockStick.enumerated() {
let x = Double(i)
if let open = each.open,
let highest = each.highest,
let lowest = each.lowest,
let close = each.close {
let candleData = CandleChartDataEntry(x: x, shadowH: highest, shadowL: lowest, open: open, close: close)
dataEntry.append(candleData)
}
}
return dataEntry
}
private func convert(dataEntry: [CandleChartDataEntry]) -> CandleChartDataSet {
let dataSet = CandleChartDataSet(entries: dataEntry)
dataSet.axisDependency = .left
dataSet.setColor(.red)
dataSet.drawIconsEnabled = false
dataSet.shadowColor = .darkGray
dataSet.shadowWidth = 0.5
dataSet.decreasingColor = .systemGreen
dataSet.decreasingFilled = true
dataSet.increasingColor = .systemRed
dataSet.increasingFilled = true
dataSet.neutralColor = .black
dataSet.drawValuesEnabled = false
return dataSet
}
private func convert(dataSet: CandleChartDataSet) -> CandleChartData {
return CandleChartData(dataSet: dataSet)
}
private func updateMaxMin(_ chartView: CandleStickChartView, dataSet: CandleChartDataSet) {
let max = dataSet.yMax
let min = dataSet.yMin
chartView.leftAxis.axisMaximum = max * 1.05
chartView.leftAxis.axisMinimum = min * 0.95
}
}
然後 K Line VC 的程式碼就會少到變成這樣,低於 50 行,而且不會 import Charts,不會和套件耦合。
import UIKit
class KLineViewController: UIViewController {
@IBOutlet weak var chartContainer: UIView!
private lazy var chartsAdapter: ChartsAdapter = {
return ChartsAdapter()
}()
private lazy var chartView: UIView = {
let view = chartsAdapter.getCandleStickChartView()
return view
}()
var kLineDataSet = [StockKLine]()
// MARK: - life cycle
override func viewDidLoad() {
super.viewDidLoad()
setupBasicUI()
setupCandleView()
}
// MARK: - private methods
private func setupBasicUI() {
chartContainer.backgroundColor = .clear
chartContainer.addSubview(chartView)
chartView.translatesAutoresizingMaskIntoConstraints = false
chartView.leadingAnchor.constraint(equalTo: chartContainer.leadingAnchor).isActive = true
chartView.topAnchor.constraint(equalTo: chartContainer.topAnchor).isActive = true
chartView.trailingAnchor.constraint(equalTo: chartContainer.trailingAnchor).isActive = true
chartView.bottomAnchor.constraint(equalTo: chartContainer.bottomAnchor).isActive = true
}
private func setupCandleView() {
chartsAdapter.update(stockSticks: kLineDataSet, on: chartView)
}
}
在Charts 套件中,可以用 IAxisValueFormatter 這個類別,來告訴 Chart View 在哪個位置要顯示什麼樣的 String。只要該類別 Conform IAxisValueFormatter,並實作 func stringForValue,告訴 Charts,就可以在 x value 顯示你要的值。
在 ChartsAdapter 內宣告 CandleXAxisValueFormatter,要求 init 代入 [Int: String]。
import UIKit
import Charts
extension ChartsAdapter {
class CandleXAxisValueFormatter: IAxisValueFormatter {
private let indexLabelMap: [Int: String]
/// 因為 candle charts 是用 index 來當 x 軸,但是 index 需要 mapping 成 date string,才可以讓人類識別每個 candle stick 代表的意義
/// - Parameter indexLabelMap: index vs. date string
init(indexLabelMap: [Int: String]) {
self.indexLabelMap = indexLabelMap
}
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
guard let string = indexLabelMap[Int(value)] else {
return ""
}
return string
}
}
}
將 Charts 的 x 軸更新的 func 如下
private func updateXAxis(_ chartView: CandleStickChartView, indexDateLabels: [Int: String]) {
chartView.xAxis.valueFormatter = CandleXAxisValueFormatter(indexLabelMap: indexDateLabels)
chartView.xAxis.granularity = 1.0
}
在 ChartsAdapter 的對外 func,將 func update(stockSticks: [StockKLine], on candleView: UIView),裡面,在完成 update 後,呼叫更新 XAxis。在 ChartsAdapter 內的 func 更改成下面這樣。
func update(stockSticks: [StockKLine], on candleView: UIView) {
let dateUtility = DateUtility()
var indexDateLabels = [Int: String]()
for (index, stick) in stockSticks.enumerated() {
if let date = stick.date {
let dateString = dateUtility.getString(date: date, format: "MM/dd")
indexDateLabels[index] = dateString
}
}
if let candleView = candleView as? CandleStickChartView {
let dataEntry = convert(stockStick: stockSticks)
let dataSet = convert(dataEntry: dataEntry)
let data = convert(dataSet: dataSet)
candleView.data = data
updateXAxis(candleView, indexDateLabels: indexDateLabels)
updateMaxMin(candleView, dataSet: dataSet)
}
}
完成的圖案如下,目標完成,剩下的間距,可以再自行細調。
下方是這次 D1 ~ D12 的完成品,可以下載來試